/* warper.cpp
 * Written by Jack Valmadre, 2008-09.
 *
 *
 *
 * More or less adheres to Google Style Guide.
 * http://google-styleguide.googlecode.com/
 */

#include <iostream>
#include <stdexcept>
#include <string>
#include <cstring>
#include <cmath>

#include <gtkmm/main.h>
#include <gdkmm/pixbuf.h>

using namespace std;

#define PI 3.14159265

// Global variable required to delete Gtk::Main instance from Python. I swear!
Gtk::Main* kit = NULL;

class Point {
 public:
  explicit Point() : x_(0.0), y_(0.0) {}

  explicit Point(double x, double y) : x_(x), y_(y) {}

  ~Point() {}

  double x() const {
    return x_;
  }

  double y() const {
    return y_;
  }

  static double Dot(const Point& a, const Point& b) {
    return a.x() * b.x() + a.y() * b.y();
  }

  double Dot(const Point& rhs) const {
    return Dot(*this, rhs);
  }

  double Norm() const {
    return sqrt(Dot(*this, *this));
  }

  string ToString() const {
    ostringstream ss;
    ss << "(" << x_ << ", " << y_ << ")";
    return ss.str();
  }

 private:
   double x_;
   double y_;
};


class Color {
 public:
  explicit Color() : red_(0), green_(0), blue_(0) {}

  explicit Color(double red, double green, double blue) : red_(red),
                                                          green_(green),
                                                          blue_(blue) {}

  explicit Color(guchar* color) : red_(color[0]),
                                  green_(color[1]),
                                  blue_(color[2]) {}

  ~Color() {}

  guchar red() const {
    return static_cast<guchar>(red_);
  }

  guchar green() const {
    return static_cast<guchar>(green_);
  }

  guchar blue() const {
    return static_cast<guchar>(blue_);
  }

  Color operator*(const double k) const {
    return Color(k * red_, k * green_, k * blue_);
  }

  Color operator+(const Color& rhs) const {
    return Color(red_ + rhs.red_, green_ + rhs.green_, blue_ + rhs.blue_);
  }

  string ToString() const {
    ostringstream ss;
    // Print color components in hexadecimal format. eg. #FFFFFF
    ss.setf(ios::hex, ios::basefield);
    ss.width(2);
    ss.fill('0');
    ss << "#" << red() << green() << blue();
    return ss.str();
  }

 private:
  double red_;
  double green_;
  double blue_;
};

Color operator*(const double k, const Color& color) {
  return color * k;
}


class InterpolationRange {
 public:
  explicit InterpolationRange(int max, bool wrap) {
    max_ = max;
    wrap_ = wrap;
  }

  ~InterpolationRange() {}

  int Constrain(int dimension) const {
    if (wrap_) {
      // Fix edge pixels for wrapping.
      dimension = dimension % max_;
      if (dimension < 0) {
        dimension += max_;
      }
    } else {
      // Clamp to bounds.
      if (dimension < 0) {
        dimension = 0;
      }
      if (dimension > max_ - 1) {
        dimension = max_ - 1;
      }
    }
    return dimension;
  }

 private:
  // Interpolation domain spans 0 to max_ - 1.
  int max_;
  // Wrap around positions that are outside the domain?
  bool wrap_;
};

class InterpolationBounds {
 public:
  explicit InterpolationBounds(double x, const InterpolationRange& range) {
    // Round down before casting to int for negative cases.
    lower_ = static_cast<int>(floor(x));
    upper_ = lower_ + 1;
    offset_ = x - lower_;
    lower_ = range.Constrain(lower_);
    upper_ = range.Constrain(upper_);
  }

  ~InterpolationBounds() {}

  template<class T>
  T Interpolate(T& a, T& b) const {
    return a * (1.0 - offset_) + b * offset_;
  }

  int lower() const {
    return lower_;
  }

  int upper() const {
    return upper_;
  }

  double offset() const {
    return offset_;
  }

 private:
  // Pixels to left and right of point (or above and below analogously).
  int lower_;
  int upper_;
  // Offset from left pixel to point.
  double offset_;
};


class Image {
 public:
  explicit Image(string filename) {
    // Attempt to open image.
    pixbuf_ = Gdk::Pixbuf::create_from_file(filename.c_str());
    // Must be 8 bits per channel.
    if (pixbuf_->get_bits_per_sample() != 8) {
      throw runtime_error("Image is not 8 bits per channel");
    }
    // Must be in RGB space.
    if (pixbuf_->get_colorspace() != Gdk::COLORSPACE_RGB) {
      throw runtime_error("Image is not RGB");
    }
  }

  explicit Image(int width, int height) {
    pixbuf_ = Gdk::Pixbuf::create(Gdk::COLORSPACE_RGB, false, 8,
                                  width, height);
  }

  ~Image() {}

  int width() const {
    return pixbuf_->get_width();
  }

  int height() const {
    return pixbuf_->get_height();
  }

  int channels() const {
    return pixbuf_->get_n_channels();
  }

  bool Contains(double x, double y) const {
    return x > -0.5 && x < static_cast<double>(width()) - 0.5 &&
           y > -0.5 && y < static_cast<double>(height()) - 0.5;
  }

  bool Contains(Point& p) const {
    return Contains(p.x(), p.y());
  }

  void Save(string filename, string type) const {
    pixbuf_->save(filename.c_str(), type.c_str());
  }

  Color GetPixel(int x, int y) const {
    return Color(addr(x, y));
  }

  Color Interpolate(double x, double y,
                    bool wrap_horizontal, bool wrap_vertical) const {
    if (!Contains(x, y)) {
      throw runtime_error("Interpolation position out of range");
    }
    InterpolationRange horizontal_range(width(), wrap_horizontal);
    InterpolationRange vertical_range(height(), wrap_vertical);
    InterpolationBounds horizontal(x, horizontal_range);
    InterpolationBounds vertical(y, vertical_range);
    // Determine pixel locations to use for interpolation.
    int left_col = horizontal.lower();
    int right_col = horizontal.upper();
    int top_row = vertical.lower();
    int bottom_row = vertical.upper();
    // Get four surrounding pixels.
    Color top_left = GetPixel(left_col, top_row);
    Color top_right = GetPixel(right_col, top_row);
    Color bottom_left = GetPixel(left_col, bottom_row);
    Color bottom_right = GetPixel(right_col, bottom_row);
    // Bilinear interpolation.
    Color top = horizontal.Interpolate(top_left, top_right);
    Color bottom = horizontal.Interpolate(bottom_left, bottom_right);
    return vertical.Interpolate(top, bottom);
  }

  Color Interpolate(Point v, bool wrap_horizontal, bool wrap_vertical) const {
    // TODO Is the copying excessively and unnecessarily?
    return Interpolate(v.x(), v.y(), wrap_horizontal, wrap_vertical);
  }

  void SetPixel(int x, int y, int r, int g, int b) {
    guchar* pixel = addr(x, y);
    pixel[0] = r;
    pixel[1] = g;
    pixel[2] = b;
  }

  void SetPixel(int x, int y, Color& p) {
    SetPixel(x, y, p.red(), p.green(), p.blue());
  }

  void SetPixel(Point& v, int r, int g, int b) {
    SetPixel(v.x(), v.y(), r, g, b);
  }

  void SetPixel(Point& v, Color& p) {
    SetPixel(v.x(), v.y(), p.red(), p.green(), p.blue());
  }
  
 protected:
  Glib::RefPtr<Gdk::Pixbuf> pixbuf_;

 private:
  guchar* addr(int x, int y) const {
    if (!Contains(x, y)) {
      throw runtime_error("Pixel position out of range");
    }
    guchar* pixels = pixbuf_->get_pixels();
    int rowstride = pixbuf_->get_rowstride();
    int channels = pixbuf_->get_n_channels();
    int index = y * rowstride + x * channels;
    return &(pixels[index]);
  }
};


class Settings {
 public:
  explicit Settings() : custom_width_(800),
                        custom_height_(600),
                        invert_x_(false),
                        invert_y_(false) {}

  ~Settings() {}

  int custom_width() const {
    return custom_width_;
  }

  void set_custom_width(int custom_width) {
    custom_width_ = custom_width;
  }

  int custom_height() const {
    return custom_height_;
  }

  void set_custom_height(int custom_height) {
    custom_height_ = custom_height;
  }

  bool invert_x() const {
    return invert_x_;
  }

  void set_invert_x(bool invert_x) {
    invert_x_ = invert_x;
  }

  bool invert_y() const {
    return invert_y_;
  }

  void set_invert_y(bool invert_y) {
    invert_y_ = invert_y;
  }

 private:
  int custom_width_;
  int custom_height_;
  bool invert_x_;
  bool invert_y_;
};


class Warper {
 public:
  explicit Warper(const Image& input,
                  int output_width,
                  int output_height,
                  int max_delta_progress,
                  double max_delta_fraction,
                  bool wrap_horizontal,
                  bool wrap_vertical) :
      input_(&input),
      output_width_(output_width),
      output_height_(output_height),
      output_(NULL),
      wrap_horizontal_(wrap_horizontal),
      wrap_vertical_(wrap_vertical),
      max_delta_progress_(max_delta_progress),
      max_delta_fraction_(max_delta_fraction),
      x_(0),
      y_(0),
      progress_(0),
      total_(output_width * output_height) {
    output_ = new Image(output_width_, output_height_);
  }

  ~Warper() {
    if (output_ != NULL) {
      delete output_;
      output_ = NULL;
    }
  }

  int input_width() const {
    return input_->width();
  }

  int input_height() const {
    return input_->height();
  }

  int output_width() const {
    return output_width_;
  }

  int output_height() const {
    return output_height_;
  }

  // Reverse mapping from output to input.
  virtual Point ReverseMap(double x, double y) const = 0;

  Point ReverseMap(const Point& dest) const {
    return ReverseMap(dest.x(), dest.y());
  }

  virtual Color Background(double x, double y) const {
    return Color(0, 0, 0);
  }

  void Step() {
    Color color;
    int delta_progress = 0;
    double delta_fraction = 0.0;

    while (delta_progress < max_delta_progress_ &&
           delta_fraction < max_delta_fraction_ &&
           progress_ < total_) {
      //cout << "(" << x_ << ", " << y_ << ")" << endl;
      // Find position in input image which maps to output image.
      // TODO Is this less efficient than returning a pointer or using new?
      Point src = ReverseMap(x_, y_);
      // Find pixel color at that position in the input image.
      if (!input_->Contains(src)) {
        color = Background(x_, y_);
      } else {
        color = input_->Interpolate(src, wrap_horizontal_, wrap_vertical_);
      }
      output_->SetPixel(x_, y_, color);
      // Increment x and y position.
      ++x_;
      if (x_ >= output_width_) {
        ++y_;
        x_ = 0;
      }
      // Increase progress counters.
      ++progress_;
      ++delta_progress;
      delta_fraction = static_cast<double>(delta_progress) / total_;
    }
  }

  void Run() {
    while (progress_ < total_) {
      Step();
    }
  }

  void Save(string filename, string format) const {
    output_->Save(filename, format);
  }

  int progess() const {
    return progress_;
  }

  int total() const {
    return total_;
  }

  double Fraction() const {
    return static_cast<double>(progress_) / total_;
  }

  bool Finished() const {
    return progress_ >= total_;
  }

 private:
  const Image* input_;
  int output_width_;
  int output_height_;
  Image* output_;
  bool wrap_horizontal_;
  bool wrap_vertical_;
  int max_delta_progress_;
  double max_delta_fraction_;
  int x_;
  int y_;
  int progress_;
  int total_;
};


class PlanetWarper : public Warper {
 public:
  explicit PlanetWarper(const Image& input,
                        const Settings& settings,
                        int max_delta_progress,
                        double max_delta_fraction) :
      Warper(input,
             settings.custom_width(),
             settings.custom_height(),
             max_delta_progress,
             max_delta_fraction,
             true,
             false) {
    settings_ = &settings;
    // Angular step between pixels.
    alpha_ = 2.0 * PI / input_width();
    // Geometric ratio between successive radii.
    k_ = 1.0 + alpha_;
    log_k_ = log(k_);
    unscaled_radius_ = ForwardMapRadius(input_height());
    center_x_ = static_cast<double>(output_width()) / 2;
    center_y_ = static_cast<double>(output_height()) / 2;
  }

  ~PlanetWarper() {
  }

  double ReverseMapRadius(double r) const {
    double v = log(r * log_k_ + 1) / log_k_;
    return v;
  }

  double ForwardMapRadius(double v) const {
    double r = (exp(v * log_k_) - 1) / log_k_;
    return r;
  }

  Point ReverseMap(double x, double y) const {
    double r;
    double theta;
    double u;
    double v;
    double custom_radius;
    // Translate to center, and invert Y axis.
    x = x - center_x_;
    y = center_y_ - y;
    // Get polar co-ords for output position.
    r = sqrt(x * x + y * y);
    theta = atan2(y, x);
    if (theta < 0) {
      theta += 2 * PI;
    }
    // Scale horizontal range to width of panoramic.
    u = theta / (2 * PI) * input_width();
    // Scale radius before starting.
    custom_radius = sqrt(output_width() * output_width() +
                         output_height() * output_height()) / 2;
    r /= custom_radius / unscaled_radius_;
    // Apply transform to radius.
    v = ReverseMapRadius(r);
    // Last step: invert axes as required.
    // To start with, y is inverted, x is non-inverted.
    if (!settings_->invert_y()) {
      v = (input_height() - 1) - v;
    }
    // Use XNOR, as inverting y inverts x.
    if ((settings_->invert_x() && settings_->invert_y()) ||
        (!settings_->invert_x() && !settings_->invert_y())) {
      u = (input_width() - 1) - u;
    }
    return Point(u, v);
  }

 private:
  const Settings* settings_;
  // Set in constructor (specific to input image)
  double alpha_;
  double k_;
  double log_k_;
  // Set in TransformSetup() (specific to output settings)
  //double max_radius_;
  double center_x_;
  double center_y_;
  double unscaled_radius_;
};


void GtkInit() {
  kit = new Gtk::Main(0, NULL);
}

void GtkDestroy() {
  if (kit != NULL) {
    delete kit;
    kit = NULL;
  }
}


class Manager {
 public:
  Manager() {
    input_ = NULL;
    warper_ = NULL;
  }

  ~Manager() {
  }

  void LoadImage(string filename) {
    if (input_ != NULL) {
      delete input_;
      input_ = NULL;
    }
    input_ = new Image(filename);
  }

  void TransformBegin(const Settings& settings,
                      int max_delta_progress,
                      double max_delta_fraction) {
    if (warper_ != NULL) {
      delete warper_;
      warper_ = NULL;
    }
    warper_ = new PlanetWarper(*input_,
                               settings,
                               max_delta_progress,
                               max_delta_fraction);
  }

  void TransformStep() {
    warper_->Step();
  }

  double TransformFraction() const {
    return warper_->Fraction();
  }

  bool TransformFinished() const {
    return warper_->Finished();
  }

  void SaveOutput(string filename, string type) const {
    warper_->Save(filename, type);
  }

 private:
  Image* input_;
  PlanetWarper* warper_;
  Settings settings_;
};

/*
int main() {
  GtkInit();

  // Load and warp image.
  Image image("../test/Grid.png");
  Settings settings;
  settings.set_custom_width(800);
  settings.set_custom_height(600);
  PlanetWarper warper(image, settings, 100000, 0.01);
  warper.Run();
  warper.Save("PlanetGrid.png", "png");

  GtkDestroy();
  return 0;
}
*/

#include <boost/python.hpp>
using namespace boost::python;

BOOST_PYTHON_MODULE(warper)
{
  def("GtkInit", GtkInit);
  def("GtkDestroy", GtkDestroy);

  class_<Settings>("Settings")
    .add_property("custom_width",
                  &Settings::custom_width, &Settings::set_custom_width)
    .add_property("custom_height",
                  &Settings::custom_height, &Settings::set_custom_height)
    .add_property("invert_x", &Settings::invert_x, &Settings::set_invert_x)
    .add_property("invert_y", &Settings::invert_y, &Settings::set_invert_y)
  ;

  class_<Manager>("Manager")
    .def("LoadImage", &Manager::LoadImage)
    .def("TransformBegin", &Manager::TransformBegin)
    .def("TransformStep", &Manager::TransformStep)
    .def("TransformFraction", &Manager::TransformFraction)
    .def("TransformFinished", &Manager::TransformFinished)
    .def("SaveOutput", &Manager::SaveOutput)
  ;
}

